#!/usr/bin/env ruby

# ---------------------------------------------------------------------------- #
# Copyright 2002-2023, OpenNebula Project, OpenNebula Systems                  #
#                                                                              #
# Licensed under the Apache License, Version 2.0 (the "License"); you may      #
# not use this file except in compliance with the License. You may obtain      #
# a copy of the License at                                                     #
#                                                                              #
# http://www.apache.org/licenses/LICENSE-2.0                                   #
#                                                                              #
# Unless required by applicable law or agreed to in writing, software          #
# distributed under the License is distributed on an "AS IS" BASIS,            #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.     #
# See the License for the specific language governing permissions and          #
# limitations under the License.                                               #
# ---------------------------------------------------------------------------- #

ONE_LOCATION = ENV['ONE_LOCATION']

if !ONE_LOCATION
    RUBY_LIB_LOCATION = '/usr/lib/one/ruby'
    GEMS_LOCATION     = '/usr/share/one/gems'
    ETC_LOCATION      = '/etc/one/'
else
    RUBY_LIB_LOCATION = ONE_LOCATION + '/lib/ruby'
    GEMS_LOCATION     = ONE_LOCATION + '/share/gems'
    ETC_LOCATION      = ONE_LOCATION + '/etc/'
end

# %%RUBYGEMS_SETUP_BEGIN%%
if File.directory?(GEMS_LOCATION)
    real_gems_path = File.realpath(GEMS_LOCATION)
    if !defined?(Gem) || Gem.path != [real_gems_path]
        $LOAD_PATH.reject! {|l| l =~ /vendor_ruby/ }

        # Suppress warnings from Rubygems
        # https://github.com/OpenNebula/one/issues/5379
        begin
            verb = $VERBOSE
            $VERBOSE = nil
            require 'rubygems'
            Gem.use_paths(real_gems_path)
        ensure
            $VERBOSE = verb
        end
    end
end
# %%RUBYGEMS_SETUP_END%%

$LOAD_PATH << RUBY_LIB_LOCATION

require 'yaml'
require 'opennebula/ldap_auth'
require 'uri'
require 'timeout'
require 'rexml/document'
require 'opennebula/error'
require 'opennebula/xml_utils'

if defined?(URI::Parser)
    URI_PARSER=URI::Parser.new
else
    URI_PARSER=URI
end

begin
    xml = OpenNebula::XMLElement.new
    xml.initialize_xml(STDIN.read, 'AUTHN')

    user   = URI_PARSER.unescape(xml['/AUTHN/USERNAME'])
    secret = URI_PARSER.unescape(xml['/AUTHN/SECRET'])
rescue StandardError
    STDERR.puts 'Invalid XML input'
    exit(-1)
end

options=YAML.load(File.read(ETC_LOCATION+'/auth/ldap_auth.conf'))

user_full = user
order, user = get_server_order(options, user)

STDERR.puts "Using group of servers: #{order.join(' ')}" if order.length>1

order.each do |servers|
    servers.each do |server_name|
        STDERR.puts "Trying LDAP server #{server_name} "

        server_conf=options[server_name]
        if !server_conf
            STDERR.puts 'Configuration for server not found'
            break
        end

        begin
            # timeout
            timeout = server_conf[:timeout]
            timeout ||= 15

            Timeout.timeout(timeout) do
                ldap=OpenNebula::LdapAuth.new(server_conf)

                user_dn, user_uid, user_group_name, memberof = ldap.find_user(user)

                if !user_dn
                    STDERR.puts "User #{user} not found"
                    break
                end

                if !user_uid.nil? && user_uid.downcase != user.downcase
                    STDERR.puts "User \"#{user}\" and \"#{user_uid}\" "\
                                'differes (leading/trailing whitespace)'
                    break
                end

                if server_conf[:group]
                    if !ldap.is_in_group?(user_group_name, server_conf[:group], memberof)
                        STDERR.puts "User #{user} is not in group #{server_conf[:group]}"
                        break
                    end
                end

                if ldap.authenticate(user_dn, secret)
                    groups = ldap.get_groups
                    if groups.empty?
                        if !server_conf[:mapping_default]
                            STDERR.puts 'User does not belong to a mapped group'
                            break
                        else
                            groups = [server_conf[:mapping_default]]
                        end
                    end

                    # authentication success
                    group_list = groups.join(' ')

                    escaped_user = URI_PARSER.escape(user_full).downcase
                    escaped_secret = URI_PARSER.escape(user_dn)

                    puts "ldap #{escaped_user} #{escaped_secret} #{group_list}"
                    exit
                else
                    STDERR.puts 'Bad user/password'
                    break
                end
            end

            # Note: break inside the Timeout block breaks only from
            # the Timeout block. To break from the servers loop,
            # we need to repeat here!
            break
        rescue SystemExit
            raise
        rescue Timeout::Error
            STDERR.puts "Communication timed out with LDAP #{server_name}"
        rescue Exception => e
            STDERR.puts "Exception raised with LDAP #{server_name}: #{e}"
        end
    end
end

# authentication failure
STDERR.puts "Could not authenticate user #{user}"
exit(-1)
